home *** CD-ROM | disk | FTP | other *** search
/ Total Network Tools 2002 / NextStepPublishing-TotalNetworkTools2002-Win95.iso / Archive / Misc Servers / Zope.exe / CATALOG.PY < prev    next >
Encoding:
Python Source  |  2000-08-15  |  22.3 KB  |  636 lines

  1. ##############################################################################
  2. # Zope Public License (ZPL) Version 1.0
  3. # -------------------------------------
  4. # Copyright (c) Digital Creations.  All rights reserved.
  5. # This license has been certified as Open Source(tm).
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions are
  8. # met:
  9. # 1. Redistributions in source code must retain the above copyright
  10. #    notice, this list of conditions, and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. #    notice, this list of conditions, and the following disclaimer in
  13. #    the documentation and/or other materials provided with the
  14. #    distribution.
  15. # 3. Digital Creations requests that attribution be given to Zope
  16. #    in any manner possible. Zope includes a "Powered by Zope"
  17. #    button that is installed by default. While it is not a license
  18. #    violation to remove this button, it is requested that the
  19. #    attribution remain. A significant investment has been put
  20. #    into Zope, and this effort will continue if the Zope community
  21. #    continues to grow. This is one way to assure that growth.
  22. # 4. All advertising materials and documentation mentioning
  23. #    features derived from or use of this software must display
  24. #    the following acknowledgement:
  25. #      "This product includes software developed by Digital Creations
  26. #      for use in the Z Object Publishing Environment
  27. #      (http://www.zope.org/)."
  28. #    In the event that the product being advertised includes an
  29. #    intact Zope distribution (with copyright and license included)
  30. #    then this clause is waived.
  31. # 5. Names associated with Zope or Digital Creations must not be used to
  32. #    endorse or promote products derived from this software without
  33. #    prior written permission from Digital Creations.
  34. # 6. Modified redistributions of any form whatsoever must retain
  35. #    the following acknowledgment:
  36. #      "This product includes software developed by Digital Creations
  37. #      for use in the Z Object Publishing Environment
  38. #      (http://www.zope.org/)."
  39. #    Intact (re-)distributions of any official Zope release do not
  40. #    require an external acknowledgement.
  41. # 7. Modifications are encouraged but must be packaged separately as
  42. #    patches to official Zope releases.  Distributions that do not
  43. #    clearly separate the patches from the original work must be clearly
  44. #    labeled as unofficial distributions.  Modifications which do not
  45. #    carry the name Zope may be packaged in any form, as long as they
  46. #    conform to all of the clauses above.
  47. # Disclaimer
  48. #   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
  49. #   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  50. #   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  51. #   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
  52. #   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  53. #   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  54. #   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  55. #   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  56. #   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  57. #   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  58. #   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  59. #   SUCH DAMAGE.
  60. # This software consists of contributions made by Digital Creations and
  61. # many individuals on behalf of Digital Creations.  Specific
  62. # attributions are listed in the accompanying credits file.
  63. ##############################################################################
  64.  
  65. from Persistence import Persistent
  66. import Acquisition
  67. import ExtensionClass
  68. import BTree, OIBTree, IOBTree, IIBTree
  69. IIBucket=IIBTree.Bucket
  70. from intSet import intSet
  71. from SearchIndex import UnIndex, UnTextIndex, UnKeywordIndex, Query
  72. from SearchIndex.Lexicon import Lexicon
  73. import regex, pdb
  74. from MultiMapping import MultiMapping
  75. from string import lower
  76. import Record
  77. from Missing import MV
  78. from zLOG import LOG, ERROR
  79.  
  80. from Lazy import LazyMap, LazyFilter, LazyCat
  81.  
  82. class NoBrainer:
  83.     """ This is the default class that gets instantiated for records
  84.     returned by a __getitem__ on the Catalog.  By default, no special
  85.     methods or attributes are defined.
  86.     """
  87.     pass
  88.  
  89. class KWMultiMapping(MultiMapping):
  90.     def has_key(self, name):
  91.         try:
  92.             r=self[name]
  93.             return 1
  94.         except KeyError:
  95.             return 0
  96.  
  97. def orify(seq,
  98.           query_map={
  99.               type(regex.compile('')): Query.Regex,
  100.               type(''): Query.String,
  101.               }):
  102.     subqueries=[]
  103.     for q in seq:
  104.         try: q=query_map[type(q)](q)
  105.         except: q=Query.Cmp(q)
  106.         subqueries.append(q)
  107.     return apply(Query.Or,tuple(subqueries))
  108.     
  109.  
  110. class Catalog(Persistent, Acquisition.Implicit, ExtensionClass.Base):
  111.     """ An Object Catalog
  112.  
  113.     An Object Catalog maintains a table of object metadata, and a
  114.     series of manageable indexes to quickly search for objects
  115.     (references in the metadata) that satisfy a search query.
  116.  
  117.     This class is not Zope specific, and can be used in any python
  118.     program to build catalogs of objects.  Note that it does require
  119.     the objects to be Persistent, and thus must be used with ZODB3.
  120.     """
  121.  
  122.     _v_brains = NoBrainer
  123.     _v_result_class = NoBrainer
  124.  
  125.     def __init__(self, vocabulary=None, brains=None):
  126.  
  127.         self.schema = {}    # mapping from attribute name to column number
  128.         self.names = ()     # sequence of column names
  129.         self.indexes = {}   # maping from index name to index object
  130.  
  131.         # The catalog maintains a BTree of object meta_data for
  132.         # convenient display on result pages.  meta_data attributes
  133.         # are turned into brain objects and returned by
  134.         # searchResults.  The indexing machinery indexes all records
  135.         # by an integer id (rid).  self.data is a mapping from the
  136.         # integer id to the meta_data, self.uids is a mapping of the
  137.         # object unique identifier to the rid, and self.paths is a
  138.         # mapping of the rid to the unique identifier.
  139.         
  140.         self.data = BTree.BTree()       # mapping of rid to meta_data
  141.         self.uids = OIBTree.BTree()     # mapping of uid to rid
  142.         self.paths = IOBTree.BTree()    # mapping of rid to uid
  143.  
  144.         # indexes can share a lexicon or have a private copy.  Here,
  145.         # we instantiate a lexicon to be shared by all text indexes.
  146.         # This may change.
  147.  
  148.         if type(vocabulary) is type(''):
  149.             self.lexicon = vocabulary
  150.         else:
  151.             #ack!
  152.             self.lexicon = Lexicon()
  153.  
  154.         if brains is not None:
  155.             self._v_brains = brains
  156.             
  157.         self.updateBrains()
  158.  
  159.     def updateBrains(self):
  160.         self.useBrains(self._v_brains)
  161.  
  162.     def __getitem__(self, index, ttype=type(())):
  163.         """
  164.         Returns instances of self._v_brains, or whatever is passed 
  165.         into self.useBrains.
  166.         """
  167.         if type(index) is ttype:
  168.             # then it contains a score...
  169.             normalized_score, score, key = index
  170.             r=self._v_result_class(self.data[key]).__of__(self.aq_parent)
  171.             r.data_record_id_ = key
  172.             r.data_record_score_ = score
  173.             r.data_record_normalized_score_ = normalized_score
  174.         else:
  175.             # otherwise no score, set all scores to 1
  176.             r=self._v_result_class(self.data[index]).__of__(self.aq_parent)
  177.             r.data_record_id_ = index
  178.             r.data_record_score_ = 1
  179.             r.data_record_normalized_score_ = 1
  180.         return r
  181.  
  182.     def __setstate__(self, state):
  183.         """ initialize your brains.  This method is called when the
  184.         catalog is first activated (from the persistent storage) """
  185.         Persistent.__setstate__(self, state)
  186.         self.updateBrains()
  187.         if not hasattr(self, 'lexicon'):
  188.             self.lexicon = Lexicon()
  189.  
  190.     def useBrains(self, brains):
  191.         """ Sets up the Catalog to return an object (ala ZTables) that
  192.         is created on the fly from the tuple stored in the self.data
  193.         Btree.
  194.         """
  195.  
  196.         class mybrains(Record.Record, Acquisition.Implicit, brains):
  197.             __doc__ = 'Data record'
  198.             def has_key(self, key):
  199.                 return self.__record_schema__.has_key(key)
  200.         
  201.         scopy={}
  202.         scopy = self.schema.copy()
  203.  
  204.         # it is useful for our brains to know these things
  205.         scopy['data_record_id_']=len(self.schema.keys())
  206.         scopy['data_record_score_']=len(self.schema.keys())+1
  207.         scopy['data_record_normalized_score_']=len(self.schema.keys())+2
  208.  
  209.         mybrains.__record_schema__ = scopy
  210.  
  211.         self._v_brains = brains
  212.         self._v_result_class=mybrains
  213.  
  214.     def addColumn(self, name, default_value=None):
  215.         """
  216.         adds a row to the meta data schema
  217.         """
  218.         
  219.         schema = self.schema
  220.         names = list(self.names)
  221.  
  222.         if schema.has_key(name):
  223.             raise 'Column Exists', 'The column exists'
  224.  
  225.         if name[0] == '_':
  226.             raise 'Invalid Meta-Data Name', \
  227.                   'Cannot cache fields beginning with "_"'
  228.         
  229.         if not schema.has_key(name):
  230.             if schema.values():
  231.                 schema[name] = max(schema.values())+1
  232.             else:
  233.                 schema[name] = 0
  234.             names.append(name)
  235.  
  236.         if default_value is None or default_value == '':
  237.             default_value = MV
  238.  
  239.         for key in self.data.keys():
  240.             rec = list(self.data[key])
  241.             rec.append(default_value)
  242.             self.data[key] = tuple(rec)
  243.  
  244.         self.names = tuple(names)
  245.         self.schema = schema
  246.  
  247.         # new column? update the brain
  248.         self.updateBrains()
  249.             
  250.         self.__changed__(1)    #why?
  251.             
  252.     def delColumn(self, name):
  253.         """
  254.         deletes a row from the meta data schema
  255.         """
  256.         names = list(self.names)
  257.         _index = names.index(name)
  258.  
  259.         if not self.schema.has_key(name):
  260.             LOG('Catalog', ERROR, ('delColumn attempted to delete '
  261.                                    'nonexistent column %s.' % str(name)))
  262.             return
  263.  
  264.         names.remove(name)
  265.  
  266.         # rebuild the schema
  267.         i=0; schema = {}
  268.         for name in names:
  269.             schema[name] = i
  270.             i = i + 1
  271.  
  272.         self.schema = schema
  273.         self.names = tuple(names)
  274.  
  275.         # update the brain
  276.         self.updateBrains()
  277.  
  278.         # remove the column value from each record
  279.         for key in self.data.keys():
  280.             rec = list(self.data[key])
  281.             rec.remove(rec[_index])
  282.             self.data[key] = tuple(rec)
  283.  
  284.     def addIndex(self, name, type):
  285.         """Create a new index, of one of the following types
  286.  
  287.         Types: 'FieldIndex', 'TextIndex', 'KeywordIndex'.
  288.         """
  289.  
  290.         if self.indexes.has_key(name):
  291.             raise 'Index Exists', 'The index specified already exists'
  292.  
  293.         if name[0] == '_':
  294.             raise 'Invalid Index Name', 'Cannot index fields beginning with "_"'
  295.  
  296.         # this is currently a succesion of hacks.  Indexes should be
  297.         # pluggable and managable
  298.  
  299.         indexes = self.indexes
  300.         if type == 'FieldIndex':
  301.             indexes[name] = UnIndex.UnIndex(name)
  302.         elif type == 'TextIndex':
  303.             indexes[name] = UnTextIndex.UnTextIndex(name, None, None,
  304.                                                     self.lexicon)
  305.         elif type == 'KeywordIndex':
  306.             indexes[name] = UnKeywordIndex.UnKeywordIndex(name)
  307.         else:
  308.             raise 'Unknown Index Type', ("%s invalid - must be one of %s"
  309.                                          % (type, ['FieldIndex', 'TextIndex',
  310.                                                    'KeywordIndex']))
  311.  
  312.         self.indexes = indexes
  313.  
  314.     def delIndex(self, name):
  315.         """ deletes an index """
  316.  
  317.         if not self.indexes.has_key(name):
  318.             raise 'No Index', 'The index specified does not exist'
  319.  
  320.         indexes = self.indexes
  321.         del indexes[name]
  322.         self.indexes = indexes
  323.  
  324.     # the cataloging API
  325.  
  326.     def catalogObject(self, object, uid, threshold=None):
  327.         """ 
  328.         Adds an object to the Catalog by iteratively applying it
  329.         all indexes.
  330.  
  331.         'object' is the object to be cataloged
  332.  
  333.         'uid' is the unique Catalog identifier for this object
  334.  
  335.         """
  336.         data = self.data
  337.  
  338.         if self.uids.has_key(uid):
  339.             i = self.uids[uid]
  340.         elif data:
  341.             i = data.keys()[-1] + 1  # find the next available unique id
  342.         else:
  343.             i = 0                       
  344.  
  345.         self.uids[uid] = i
  346.         self.paths[i] = uid
  347.         
  348.         # meta_data is stored as a tuple for efficiency
  349.         data[i] = self.recordify(object)
  350.  
  351.         total = 0
  352.         for x in self.indexes.values():
  353.             ## tricky!  indexes need to acquire now, and because they
  354.             ## are in a standard dict __getattr__ isn't used, so
  355.             ## acquisition doesn't kick in, we must explicitly wrap!
  356.             x = x.__of__(self)
  357.             if hasattr(x, 'index_object'):
  358.                 blah = x.index_object(i, object, threshold)
  359.                 total = total + blah
  360.             else:
  361.                 LOG('Catalog', ERROR, ('catalogObject was passed '
  362.                                        'bad index object %s.' % str(x)))
  363.  
  364.         self.data = data
  365.  
  366.         return total
  367.  
  368.     def uncatalogObject(self, uid):
  369.         """ 
  370.         Uncatalog and object from the Catalog.  and 'uid' is a unique
  371.         Catalog identifier
  372.  
  373.         Note, the uid must be the same as when the object was
  374.         catalogued, otherwise it will not get removed from the catalog
  375.  
  376.         This method should not raise an exception if the uid cannot
  377.         be found in the catalog.
  378.  
  379.         """
  380.         data = self.data
  381.         uids = self.uids
  382.         paths = self.paths
  383.         indexes = self.indexes
  384.         rid = uids.get(uid, None)
  385.  
  386.         if rid is not None:
  387.             for x in indexes.values():
  388.                 x = x.__of__(self)
  389.                 if hasattr(x, 'unindex_object'):
  390.                     x.unindex_object(rid)
  391.                     # this should never raise an exception
  392.             for btree in (data, paths):
  393.                 try:
  394.                     del btree[rid]
  395.                 except KeyError:
  396.                     LOG('Catalog', ERROR, ('uncatalogObject unsuccessfully '
  397.                                            'attempted to delete rid %s '
  398.                                            'from paths or data btree.' % rid))
  399.             del uids[uid]
  400.             self.data = data
  401.         else:
  402.             LOG('Catalog', ERROR, ('uncatalogObject unsuccessfully '
  403.                                    'attempted to uncatalog an object '
  404.                                    'with a uid of %s. ' % uid))
  405.             
  406.     def clear(self):
  407.         """ clear catalog """
  408.         
  409.         self.data = BTree.BTree()
  410.         self.uids = OIBTree.BTree()
  411.         self.paths = IOBTree.BTree()
  412.  
  413.         for x in self.indexes.values():
  414.             x.clear()
  415.  
  416.     def uniqueValuesFor(self, name):
  417.         """ return unique values for FieldIndex name """
  418.         return self.indexes[name].uniqueValues()
  419.  
  420.     def hasuid(self, uid):
  421.         """ return the rid if catalog contains an object with uid """
  422.         if self.uids.has_key(uid):
  423.             return self.uids[uid]
  424.         else:
  425.             return None
  426.  
  427.     def recordify(self, object):
  428.         """ turns an object into a record tuple """
  429.  
  430.         record = []
  431.         # the unique id is allways the first element
  432.         for x in self.names:
  433.             try:
  434.                 attr = getattr(object, x)
  435.                 if(callable(attr)):
  436.                     attr = attr()
  437.                     
  438.             except:
  439.                 attr = MV
  440.             record.append(attr)
  441.  
  442.         return tuple(record)
  443.  
  444.     def instantiate(self, record):
  445.  
  446.         r=self._v_result_class(record[1])
  447.         r.data_record_id_ = record[0]
  448.         return r.__of__(self)
  449.  
  450.  
  451. ## Searching engine.  You don't really have to worry about what goes
  452. ## on below here...  Most of this stuff came from ZTables with tweaks.
  453.  
  454.     def _indexedSearch(self, args, sort_index, append, used,
  455.                        IIBType=type(IIBucket()), intSType=type(intSet())):
  456.         """
  457.         Iterate through the indexes, applying the query to each one.
  458.         Do some magic to join result sets.  Be intelligent about
  459.         handling intSets and IIBuckets.
  460.         """
  461.  
  462. ##        import pdb
  463. ##        pdb.set_trace()
  464.  
  465.         ## I use this so much I'm just leaving it commented out -michel
  466.  
  467.         rs=None
  468.         data=self.data
  469.         
  470.         if used is None: used={}
  471.         for i in self.indexes.keys():
  472.             try:
  473.                 index = self.indexes[i].__of__(self)
  474.                 if hasattr(index,'_apply_index'):
  475.                     r=index._apply_index(args)
  476.                     if r is not None:
  477.                         r, u = r
  478.                         for name in u:
  479.                             used[name]=1
  480.                         if rs is None:
  481.                             rs = r
  482.                         else:
  483.                             # you can't intersect an IIBucket into an
  484.                             # intSet, but you can go the other way
  485.                             # around.  Make sure we're facing the
  486.                             # right direction...
  487.                             if type(rs) is intSType and type(r) is IIBType:
  488.                                 rs=r.intersection(rs)
  489.                             else:
  490.                                 rs=rs.intersection(r)
  491.             except:
  492.                 return used
  493.  
  494.         if rs is None:
  495.             if sort_index is None:
  496.                 rs=data.items()
  497.                 append(LazyMap(self.instantiate, rs))
  498.             else:
  499.                 for k, intset in sort_index._index.items():
  500.                     append((k,LazyMap(self.__getitem__, intset)))
  501.         elif rs:
  502.             if sort_index is None and type(rs) is IIBType:
  503.                 # then there is score information.  Build a new result 
  504.                 # set, sort it by score, reverse it, compute the
  505.                 # normalized score, and Lazify it.
  506.                 rset = []
  507.                 for key, score in rs.items():
  508.                     rset.append((score, key))
  509.                 rset.sort()
  510.                 rset.reverse()
  511.                 max = float(rset[0][0])
  512.                 rs = []
  513.                 for score, key in rset:
  514.                     rs.append(( int((score/max)*100), score, key))
  515.                 append(LazyMap(self.__getitem__, rs))
  516.                     
  517.             elif sort_index is None and type(rs) is intSType:
  518.                 # no scores?  Just Lazify.
  519.                 append(LazyMap(self.__getitem__, rs))
  520.             else:
  521.                 # sort.  If there are scores, then this block is not
  522.                 # reached, therefor 'sort-on' does not happen in the
  523.                 # context of text index query.  This should probably
  524.                 # sort by relevance first, then the 'sort-on' attribute.
  525.                 if len(rs)>len(sort_index._index):
  526.                     for k, intset in sort_index._index.items():
  527.                         if type(rs) is IIBType:
  528.                             intset=rs.intersection(intset)
  529.                         else:
  530.                             intset=intset.intersection(rs)
  531.                         if intset: 
  532.                             append((k,LazyMap(self.__getitem__, intset)))
  533.                 else:
  534.                     for r in rs:
  535.                         append((sort_index._unindex[r],
  536.                                LazyMap(self.__getitem__,[r])))
  537.  
  538.         return used
  539.  
  540.     def searchResults(self, REQUEST=None, used=None,
  541.                       query_map={
  542.                           type(regex.compile('')): Query.Regex,
  543.                           type([]): orify,
  544.                           type(''): Query.String,
  545.                           }, **kw):
  546.  
  547.  
  548.  
  549.         # Get search arguments:
  550.         if REQUEST is None and not kw:
  551.             try: REQUEST=self.REQUEST
  552.             except: pass
  553.         if kw:
  554.             if REQUEST:
  555.                 m=KWMultiMapping()
  556.                 m.push(REQUEST)
  557.                 m.push(kw)
  558.                 kw=m
  559.         elif REQUEST: kw=REQUEST
  560.  
  561.         # Make sure batch size is set
  562.         if REQUEST and not REQUEST.has_key('batch_size'):
  563.             try: batch_size=self.default_batch_size
  564.             except: batch_size=20
  565.             REQUEST['batch_size']=batch_size
  566.  
  567.         # Compute "sort_index", which is a sort index, or none:
  568.         if kw.has_key('sort-on'):
  569.             sort_index=kw['sort-on']
  570.         elif hasattr(self, 'sort-on'):
  571.             sort_index=getattr(self, 'sort-on')
  572.         elif kw.has_key('sort_on'):
  573.             sort_index=kw['sort_on']
  574.         else: sort_index=None
  575.         sort_order=''
  576.         if sort_index is not None and sort_index in self.indexes.keys():
  577.             sort_index=self.indexes[sort_index]
  578.  
  579.         # Perform searches with indexes and sort_index
  580.         r=[]
  581.         used=self._indexedSearch(kw, sort_index, r.append, used)
  582.         if not r: return r
  583.  
  584.         # Sort/merge sub-results
  585.         if len(r)==1:
  586.             if sort_index is None: r=r[0]
  587.             else: r=r[0][1]
  588.         else:
  589.             if sort_index is None: r=LazyCat(r)
  590.             else:
  591.                 r.sort()
  592.                 if kw.has_key('sort-order'):
  593.                     so=kw['sort-order']
  594.                 elif hasattr(self, 'sort-order'):
  595.                     so=getattr(self, 'sort-order')
  596.                 elif kw.has_key('sort_order'):
  597.                     so=kw['sort_order']
  598.                 else: so=None
  599.                 if (type(so) is type('') and
  600.                     lower(so) in ('reverse', 'descending')):
  601.                     r.reverse()
  602.                 r=LazyCat(map(lambda i: i[1], r))
  603.  
  604.         return r
  605.  
  606.     __call__ = searchResults
  607.  
  608.  
  609.  
  610.  
  611.  
  612.  
  613.  
  614.  
  615.